/*
 *   Copyright 2012, Thomas Kerber
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package milk.jpatch.fileLevel;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;

import milk.jpatch.Util;

/**
 * Patches all a directory structure.
 * @author Thomas Kerber
 * @version 1.1.2
 */
public class FilesPatch{
    
    /**
     * The component file patches.
     */
    private Map<String, FilePatch> filePatches;
    
    /**
     * Creates.
     * @param filePatches The component file patches.
     */
    public FilesPatch(Map<String, FilePatch> filePatches){
        this.filePatches = filePatches;
    }
    
    /**
     * "Deserializes"
     * @param root The root directory to load patches from.
     * @throws IOException If a read error occurs.
     */
    public FilesPatch(File root) throws IOException{
        filePatches = new HashMap<String, FilePatch>();
        loadFromDir(root, root);
    }
    
    /**
     * Generates.
     * @param rootOrig The root of the original.
     * @param rootMod The root of the modification.
     * @return The files patch.
     * @throws IOException If a read error occurrs.
     */
    public static FilesPatch generate(File rootOrig, File rootMod)
            throws IOException{
        Set<String> names = new HashSet<String>();
        names.addAll(Util.listNamesIn(rootOrig));
        names.addAll(Util.listNamesIn(rootMod));
        Map<String, FilePatch> patches = new HashMap<String, FilePatch>();
        for(String name : names){
            patches.put(name, FilePatch.generate(rootOrig, rootMod, name));
        }
        return new FilesPatch(patches);
    }
    
    /**
     * Applies a series of FilesPatches.
     * @param rootIn The input root.
     * @param rootOut The output root.
     * @param patches The patches.
     * @throws IOException
     */
    public static void patchAll(File rootIn, File rootOut,
            FilesPatch[] patches) throws IOException{
        Set<String> names = new HashSet<String>(Util.listNamesIn(rootIn));
        for(FilesPatch fp : patches)
            names.addAll(fp.filePatches.keySet());
        
        for(String name : names){
            File inFile = new File(rootIn, name);
            File outFile = new File(rootOut, name);
            outFile.getParentFile().mkdirs();
            try{
                patchName(name, inFile, outFile, patches, patches.length - 1);
            }
            catch(Exception e){
                Util.logger.log(Level.SEVERE,
                        "Patch of " + name + " failed. Falling back to ID.", e);
                new FileID(name).patch(inFile, outFile);
            }
            
        }
    }
    
    /**
     * Applies a series of patches to a certain name.
     * @param name The name to apply to.
     * @param inFile The input file.
     * @param outFile The output file.
     * @param patches The patches to apply.
     * @param lastIndex The index of the last patch to apply.
     * @throws IOException
     */
    private static void patchName(String name, File inFile, File outFile,
            FilesPatch[] patches, int lastIndex) throws IOException{
        
        // So that sanity may be kept.
        if(lastIndex == -1){
            new FileID(name).patch(inFile, outFile);
            return;
        }
        
        FilePatch fp = patches[lastIndex].getPatchForName(name);
        
        if(fp instanceof FileID){
            // Do nothing and continue to the previous patch.
            patchName(name, inFile, outFile, patches, lastIndex - 1);
        }
        else if(fp instanceof FileRemove){
            // Just do nothing.
        }
        else if(fp instanceof FileReplace){
            // Apply the patch. And then do nothing.
            fp.patch(inFile, outFile);
        }
        else if(fp instanceof ClassPatch){
            // YAY! This is special.
            JavaClass c = patchClassName(name, inFile, patches, lastIndex);
            c.dump(outFile);
        }
    }
    
    /**
     * Applies a series of patches to a class.
     * @param name The name to patch.
     * @param inFile The input file.
     * @param patches The patches to apply.
     * @param lastIndex The index of the last patch to apply.
     * @return The patched class.
     * @throws IOException
     */
    private static JavaClass patchClassName(String name, File inFile,
            FilesPatch[] patches, int lastIndex) throws IOException{
        if(lastIndex == -1){
            return new ClassParser(inFile.getAbsolutePath()).parse();
        }
        
        FilePatch fp = patches[lastIndex].getPatchForName(name);
        
        if(fp instanceof ClassPatch){
            JavaClass prev = patchClassName(name, inFile, patches,
                    lastIndex - 1);
            return ((ClassPatch)fp).patch(prev);
        }
        else if(fp instanceof FileID)
            return patchClassName(name, inFile, patches, lastIndex - 1);
        else{
            File prev = File.createTempFile("milk", ".class");
            patchName(name, inFile, prev, patches, lastIndex);
            return new ClassParser(prev.getAbsolutePath()).parse();
        }
    }
    
    /**
     * Patches all files in a directory.
     * @param rootIn The input dir.
     * @param rootOut The output dir.
     * @throws IOException
     */
    public void patchAll(File rootIn, File rootOut) throws IOException{
        FilesPatch.patchAll(rootIn, rootOut, new FilesPatch[]{this});
    }
    
    /**
     * Gets patch for a certain file.
     * 
     * If none is found, an ID patch is generated.
     * @param name The filename.
     * @return The file patch.
     */
    public FilePatch getPatchForName(String name){
        if(!filePatches.containsKey(name)){
            // Yes, we hate manifest files.
            // TODO: option to turn this off.
            if(name.endsWith("MANIFEST.MF"))
                filePatches.put(name, new FileRemove(name));
            else
                filePatches.put(name, new FileID(name));
        }
        return filePatches.get(name);
    }
    
    /**
     * Deserializes recursively in a directory.
     * @param root The root directory.
     * @param dirAt The currently deserializing directory.
     * @throws IOException
     */
    private void loadFromDir(File root, File dirAt) throws IOException{
        for(File f : dirAt.listFiles()){
            if(f.isDirectory())
                loadFromDir(root, f);
            else if(f.isFile()){
                FilePatch fp = null;
                if(ClassPatch.canDeserializeAt(f)){
                    fp = ClassPatch.deserializeAt(root, f);
                }
                else if(FileRemove.canDeserializeAt(f)){
                    fp = FileRemove.deserializeAt(root, f);
                }
                else if(FileReplace.canDeserializeAt(f)){
                    fp = FileReplace.deserializeAt(root, f);
                }
                // Note: FileID is redundant, as it doesn't do anything.
                filePatches.put(fp.name, fp);
            }
        }
    }
    
    /**
     * Dumps to directory.
     * @param dir The directory to dump to.
     * @throws IOException If a write error occurs.
     */
    public void serializeToDir(File dir) throws IOException{
        for(FilePatch fp : filePatches.values()){
            fp.serialize(dir);
        }
    }
    
    /**
     * Dumps to a zip file.
     * @param zip The zip file.
     * @throws IOException If a read/write error occurs.
     */
    public void serializeToZip(File zip) throws IOException{
        // TODO: possibly stream directly to zip?
        File dir = Util.getTempDir();
        serializeToDir(dir);
        Util.packZip(zip, dir);
        Util.remDir(dir);
    }
    
}
